using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;

namespace mebc4.tablebased
{
    class MebcRuntime : IDisposable
    {
        private readonly Dictionary<string, List<string>> mappings = new Dictionary<string, List<string>>();
        private readonly Dictionary<string, Delegate> inputPins = new Dictionary<string, Delegate>();

        private readonly MessageCenter messages = new MessageCenter();
        private bool processMessages = true;
        private readonly ManualResetEvent stopProcessingFlag = new ManualResetEvent(false);
        private readonly List<Thread> _processorThreads = new List<Thread>();


        public void Map(string outputPinName, string inputPinName)
        {
            List<string> mapping;
            if (!mappings.TryGetValue(outputPinName, out mapping)) {
                mapping = new List<string>();
                mappings.Add(outputPinName, mapping); }

            mapping.Add(inputPinName);
            messages.AddPin(outputPinName);
        }


        public void RegFunc<TInput, TOutput>(string name, Func<TInput, TOutput> func) { Reg<TInput>(name, x => Post(name, func(x))); }
        public void RegProc<TInput>(string name, Action<TInput> proc) { Reg(name, proc); }
        public void RegProc<TInput, TOutput>(string name, Action<TInput, Action<TOutput>> proc) { Reg<TInput>(name, x => proc(x, o => Post(name, o))); }
        //TODO: Name output pins according to output param names? What if proc is a lambda expression without readable param names?
        public void RegProc<TInput, TOutput0, TOutput1>(string name, Action<TInput, Action<TOutput0>, Action<TOutput1>> proc)
        {
            Reg<TInput>(name, x => proc(x, o => Post(name + "0", o), o => Post(name + "1", o)));
        }
        public void Reg<TInput>(string inputPinName, Action<TInput> processor) { inputPins.Add(inputPinName, processor); }
        public void Reg(string inputPinName, Delegate processor) { inputPins.Add(inputPinName, processor); }


        public void RegEventBasedOp(object op)
        {
            var methods = FindInstanceMethods(op);
            RegMethods(methods, op);

            var events = FindEvents(op);
            RegEvents(events, op);
        }


        public void RegInstanceMethods(object instance)
        {
            var methods = FindInstanceMethods(instance);
            RegMethods(methods, instance);
        }

        public void RegStaticMethods<T>() { RegStaticMethods(typeof(T)); }
        public void RegStaticMethods(Type type)
        {
            var methods = FindStaticMethods(type);
            RegMethods(methods, null);
        }

        internal void RegMethods(IEnumerable<MethodInfo> methods, object instance)
        {
            methods.ToList()
                   .ForEach(mi =>
                             {
                                 var opName = mi.DeclaringType.Name + "." + mi.Name;

                                 if (IsFunction(mi))
                                     Reg<object>(opName, x => Post(opName, mi.Invoke(instance, new[] { x })));
                                 else if (mi.GetParameters().Count() == 1)
                                     Reg<object>(opName, x => mi.Invoke(instance, new[] { x }));
                                 else if (mi.GetParameters().Count() == 2)
                                 {
                                     var output = BuildOutputAction(mi.GetParameters()[1].ParameterType.GetGenericArguments()[0], opName);
                                     Reg<object>(opName, x => mi.Invoke(instance, new[] { x, output }));
                                 }
                                 else if (mi.GetParameters().Count() == 3)
                                 {
                                     var outputName0 = opName + "." + mi.GetParameters()[1].Name;
                                     var output0 = BuildOutputAction(mi.GetParameters()[1].ParameterType.GetGenericArguments()[0], outputName0);
                                     var outputName1 = opName + "." + mi.GetParameters()[2].Name;
                                     var output1 = BuildOutputAction(mi.GetParameters()[2].ParameterType.GetGenericArguments()[0], outputName1);
                                     Reg<object>(opName, x => mi.Invoke(instance, new[] { x, output0, output1 }));
                                 }
                                 else
                                     throw new InvalidOperationException("Cannot register procedure " + opName);
                             });
        }

        internal Delegate BuildOutputAction(Type paramType, string opName)
        {
            var param = Expression.Parameter(paramType);
            var body = Expression.Call(Expression.Constant(this), this.GetType().GetMethod("Post"), 
                                       Expression.Constant(opName), 
                                       Expression.TypeAs(param, typeof(object)));
            var lambda = Expression.Lambda(typeof(Action<>).MakeGenericType(paramType), body, param);
            return lambda.Compile();            
        }

        internal void RegEvents(IEnumerable<EventInfo> events, object instance)
        {
            events.ToList()
                   .ForEach(ei =>
                   {
                       var opName = ei.DeclaringType.Name + "." + ei.Name;

                       Action<object> output = o => Post(opName, o);
                       ei.AddEventHandler(instance, output);
                   });
        }

        internal static IEnumerable<MethodInfo> FindInstanceMethods(object instance)
        {
            return instance.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public)
                                     .Where(mi => mi.DeclaringType == instance.GetType());
        }

        internal static IEnumerable<EventInfo> FindEvents(object instance)
        {
            return instance.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public);
        }

        internal static IEnumerable<MethodInfo> FindStaticMethods(Type type)
        {
            return type.GetMethods(BindingFlags.Static | BindingFlags.Public);
        }

        internal static bool IsFunction(MethodInfo mi)
        {
            return mi.ReturnType != typeof(void);
        }


        private void Send(string outputPinName, object message)
        {
            Console.WriteLine("   send({0}, {1}), thread {2}", outputPinName, message!=null ? message.GetType().Name : "NULL", Thread.CurrentThread.GetHashCode());

            var mapping = mappings[outputPinName];
            mapping.ForEach(m =>{
                                    var p = inputPins[m];
                                    p.DynamicInvoke(message);
                                });

            messages.Unblock(outputPinName);
        }


        public void Post(string outputPinName, object message)
        {
            if (_processorThreads.Count > 0)
                messages.Enqueue(outputPinName, message);
            else
                Send(outputPinName, message);
        }


        public void Run() { Run(1); }
        public void Run(int numberOfThreads)
        {
            for (var i = 0; i < numberOfThreads; i++)
            {
                var th = new Thread(() => {
                    while (processMessages)
                    {
                        Tuple<string, object> pinMessage;
                        while (messages.TryBlockingDequeue(out pinMessage))
                        {
                            Send(pinMessage.Item1, pinMessage.Item2);
                        }
                        WaitHandle.WaitAny(new WaitHandle[]{stopProcessingFlag, messages.MessageReadyFlag});
                    }});
                th.IsBackground = true;
                th.Start();
                _processorThreads.Add(th);
            }
        }


        public void Dispose()
        {
            processMessages = false;
            stopProcessingFlag.Set();
            messages.Dispose();
            _processorThreads.ForEach(th => th.Join());
        }
    }


    class MessageCenter : IDisposable
    {
        public Dictionary<string, BlockableConcurrentQueue<object>> _pinQueues = new Dictionary<string, BlockableConcurrentQueue<object>>();
        public ConcurrentRing<string> _pinRing = new ConcurrentRing<string>();
        public AutoResetEvent _newMessageFlag = new AutoResetEvent(false);


        public void Enqueue(string pinName, object message)
        {
            _pinQueues[pinName].Enqueue(message);
            _newMessageFlag.Set();
        }

        public bool TryBlockingDequeue(out Tuple<string, object> pinMessage)
        {
            foreach (var pinName in _pinRing.Turn()) {
                var pinQueue = _pinQueues[pinName];
                object message;
                if (pinQueue.TryBlockingDequeue(out message)) {
                    pinMessage = new Tuple<string, object>(pinName, message);
                    return true; }}

            pinMessage = null;
            return false;
        }


        public AutoResetEvent MessageReadyFlag
        {
            get { return _newMessageFlag; }
        }


        public void AddPin(string pinName)
        {
            _pinQueues.Add(pinName, new BlockableConcurrentQueue<object>());
            _pinRing.Add(pinName);
        }


        public void Unblock(string pinName)
        {
            _pinQueues[pinName].Unblock();
        }


        public void Dispose()
        {
            _newMessageFlag.Set();
        }
    }


    class BlockableConcurrentQueue<T>
    {
        private readonly Queue<T> _queue = new Queue<T>();
        private bool _blocked;

        public void Enqueue(T item)
        {
            lock (_queue) {
                _queue.Enqueue(item); }
        }

        public bool TryBlockingDequeue(out T item)
        {
            lock (_queue) {
                if (_queue.Count == 0 || _blocked) {
                    item = default(T);
                    return false; }

                item = _queue.Dequeue();
                _blocked = true;
                return true; }
        }

        public void Unblock()
        {
            lock (_queue) {
                _blocked = false; }
        }
    }


    class ConcurrentRing<T>
    {
        private readonly Queue<T> _queue = new Queue<T>();

        public void Add(T item)
        {
            lock (_queue) {
                if (_queue.Contains(item)) return;
                _queue.Enqueue(item); }
        }

        public T[] Turn()
        {
            lock (_queue) {
                var itemValues = _queue.ToArray();

                T nextItem = _queue.Dequeue();
                _queue.Enqueue(nextItem);

                return itemValues; }
        }
    }
}